using NetMQ;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;

namespace ParallelLib.Micro
{
    public class Beacon : IDisposable
    {
        private readonly TimeSpan _beaconPubInterval = TimeSpan.FromSeconds(1);
        private readonly TimeSpan _checkDeadNodeTimerInterval = TimeSpan.FromSeconds(1);
        private readonly TimeSpan _deadNodeTimeout = TimeSpan.FromSeconds(5);
        private readonly BeaconNode _node = new BeaconNode();
        private readonly Dictionary<BeaconNode, DateTime> _nodes = new Dictionary<BeaconNode, DateTime>();
        private NetMQBeacon _beacon;
        private NetMQPoller _poller;
        private NetMQTimer _timer;

        public Beacon(string id, string name, string type, string group)
            : this(id, name, type, group, 12321)
        {
        }

        public Beacon(string id, string name, string type, string group, int port)
        {
            this._node.Id = id;
            this._node.Type = type;
            this._node.Name = name;
            this._node.Group = group;
            this.Port = port;
            InitBeacon();
        }

        public event Action<Beacon, BeaconNode> ConnectedNode;

        public event Action<Beacon, BeaconNode> DisConnectedNode;

        public string Group
        {
            get { return _node.Group; }
        }

        public bool IsRunning
        {
            get { return _poller != null && _poller.IsRunning; }
        }

        public string Name
        {
            get { return _node.Name; }
        }

        public int Port { get; private set; }

        public BeaconNode SelfNode
        {
            get { return _node; }
        }

        public void Dispose()
        {
            _nodes.Clear();
            if (_beacon != null)
            {
                _beacon.Dispose();
                _beacon = null;
            }
            if (_poller != null)
            {
                _poller.Dispose();
                _poller = null;
            }
        }

        public List<BeaconNode> GetAllLiveNodes()
        {
            return _nodes.Keys.ToList();
        }

        public void Start()
        {
            InitBeacon();
            if (IsRunning)
                return;
            _beacon.Subscribe("");
            _beacon.Publish(_node.Serialize(), _beaconPubInterval);
            _timer = new NetMQTimer(_checkDeadNodeTimerInterval);
            _timer.Elapsed += ClearDeadNodes;
            _poller = new NetMQPoller { _beacon, _timer };
            _poller.RunAsync();
        }

        public void Stop()
        {
            if (!IsRunning)
                return;
            this._nodes.Clear();
            _poller.Stop();
            _beacon.Unsubscribe();
            _poller.Remove(_beacon);
            _poller.Remove(_timer);
            _poller.Dispose();
            _poller = null;
            _beacon.Dispose();
            _beacon = null;
            _timer = null;
        }

        private void ClearDeadNodes(object sender, NetMQTimerEventArgs e)
        {
            var deadNodes = _nodes.
                Where(n => DateTime.Now > n.Value + _deadNodeTimeout)
                .Select(n => n.Key).ToArray();
            if (!deadNodes.Any())
                return;
            foreach (var node in deadNodes)
            {
                _nodes.Remove(node);
                OnNodeDisConnected(node);
            }
        }

        private string GetAvalibleAddress()
        {
            string address = "*";
            string configAddress = ConfigurationManager.AppSettings["BeaconAddr"];
            if (configAddress != "*"
                && configAddress != "127.0.0.1"
                && configAddress != "loopback"
                && Utils.GetAddress().Any()
                && Utils.GetAddress().All(e => e.Address.ToString() != configAddress))
                configAddress = Utils.GetAddress().First().Address.ToString();
            if (string.IsNullOrEmpty(configAddress))
            {
                var addressQuery = Utils.GetAddress();
                if (addressQuery.Any())
                    address = addressQuery.First().Address.ToString();
            }
            else
                address = configAddress;
            return address;
        }

        private void InitBeacon()
        {
            if (_beacon == null)
            {
                _beacon = new NetMQBeacon();
                _beacon.Configure(Port, GetAvalibleAddress());
                _beacon.Subscribe("");
                _beacon.ReceiveReady += OnBeaconReady;
                _node.HostName = _beacon.HostName;
                _node.ArgumentsChanged += OnBeaconArgumentsChanged;
            }
        }

        private void OnBeaconArgumentsChanged(BeaconNode obj)
        {
            if (this.IsRunning)
                _beacon.Publish(_node.Serialize(), TimeSpan.FromSeconds(1));
        }

        private void OnBeaconReady(object sender, NetMQBeaconEventArgs e)
        {
            var message = e.Beacon.Receive();
            if (message.Bytes == null || message.Bytes.Length == 0)
                return;
            BeaconNode node;
            try
            {
                node = JsonConvert.DeserializeObject<BeaconNode>(message.String);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.TraceError(ex.Message);
                node = new BeaconNode() { Arguments = message.String };
            }
            node.Address = message.PeerAddress;
            if (!_nodes.ContainsKey(node))
            {
                _nodes.Add(node, DateTime.Now);

                OnNodeConnected(node);
            }
            else
            {
                _nodes[node] = DateTime.Now;
            }
        }

        private void OnNodeConnected(BeaconNode node)
        {
            if (node == null)
                return;
            ConnectedNode?.Invoke(this, node);
        }

        private void OnNodeDisConnected(BeaconNode node)
        {
            if (node != null && this.DisConnectedNode != null)
                DisConnectedNode?.Invoke(this, node);
        }
    }
}